package com.zhongpengcheng.blog.auth.filter;

import com.zhongpengcheng.blog.auth.AuthToken;
import com.zhongpengcheng.blog.util.RequestUtils;
import com.zhongpengcheng.blog.util.ResponseUtils;
import com.zhongpengcheng.blog.util.ResultUtils;
import com.zhongpengcheng.blog.util.TokenUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.CredentialsException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

import static com.zhongpengcheng.blog.enums.ResultCodeEnum.ERROR;

/**
 * 鉴权拦截器
 * note: 1.创建 by zhongpengcheng
 *
 * @author zhongpengcheng
 * @date 2021/08/13 17:03
 **/
@Slf4j
public class AuthFilter extends AuthenticatingFilter {

    private final ErrorProperties errorProperties;

    public AuthFilter(ErrorProperties errorProperties) {
        this.errorProperties = errorProperties;
    }

    /**
     * 构造shiro的token对象
     * @return token对象
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
        String token =  TokenUtils.getToken(request);
        if (token == null) {
            return null;
        }
        return new AuthToken(token);
    }

    /**
     * 当请求被阻拦时的处理校验逻辑
     * @return 放行返回true，否则返回false
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        // 只有需要鉴权的请求会走到这一步，登陆请求不会被该拦截器拦截
        return executeLogin(request, response);
    }

    /**
     * 执行认证逻辑，包括登录和token认证
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        AuthenticationToken token = null;
        try {
            token = createToken(request, response);
            if (token == null) {
                log.error("missing token in request");
                throw new CredentialsException("request中没有token");
            }
            Subject subject = getSubject(request, response);
            subject.login(token);
            return onLoginSuccess(token, subject, request, response);
        } catch (AuthenticationException e) {
            // 登录失败，走失败逻辑
            return onLoginFailure(token, e, request, response);
        }
    }

    /**
     * 是否允许响应请求，这里拦截除了OPTIONS类型请求以外的一切请求
     * @param mappedValue 映射数据
     * @return 允许响应返回true，否则返回false
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (WebUtils.toHttp(request).getMethod().equals(RequestMethod.OPTIONS.name())) {
            return true;
        }
        log.info("source ip={}, request url={}, has token={}", RequestUtils.getNginxRealIP(request),
                WebUtils.toHttp(request).getRequestURL().toString(),
                StringUtils.isNotBlank(TokenUtils.getToken(request)));
        return false;
    }

    /**
     * 登录失败处理逻辑
     * @param token 认证用token
     * @param e 失败异常
     * @return 是否放行访问（即登陆失败仍能继续访问），默认为false不放行
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        try {
            request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, e);
            request.getRequestDispatcher(errorProperties.getPath()).forward(request, response);
        } catch (ServletException | IOException ex) {
            ResponseUtils.write(WebUtils.toHttp(response), ResultUtils.failed(ERROR));
        }
        return false;
    }
}
